#include "leam-stream.hh"
#include "leam-enums.hh"
#include <iostream>

using namespace std;
using namespace licada;
using namespace boost;

namespace leam
{	
    LeamStream::LeamStream( ILeamInStream* _inStream, ILeamOutStream* _outStream
			    , ILeamHandler* _handler, bool checkVersion )
    {
	setup( _inStream, _outStream, _handler, checkVersion );
    }
    void LeamStream::setup( ILeamInStream* _inStream, ILeamOutStream* _outStream
		, ILeamHandler* _handler, bool checkVersion )
    {
	curId = 1;
	inStream = _inStream;
	outStream = _outStream;
	handler = _handler;
	unAck.clear();
	unFin.clear();
	if ( inStream && checkVersion ) {
	    uint16 version;
	    *outStream << (uint16)ELeamVersionCurrent;
	    *inStream >> version;
	    if ( version != ELeamVersionCurrent )
		throw LeamVersionMismatch( version );
	}
    }
    
    LeamStream::~LeamStream() {}
    
    TBeginMessageReturn LeamStream::beginSendMessage( uint32 length )
    {
	LeamMessageHeader hdr;
	hdr.type = ELeamTypeMessage;
	hdr.msgId = curId;
	hdr.length = length;
	++curId;
	*outStream << hdr.type;
	*outStream << hdr.msgId;
	*outStream << hdr.length;
	dataOut.setup( length, outStream );
	unAck.push_back( hdr.msgId );
	unFin.push_back( hdr.msgId );
	return TBeginMessageReturn( hdr.msgId, &dataOut );
    }
    static void beginError( TType errorType, ILeamOutStream* outStream, TMessageId msgId )
    {
	*outStream << (TType)errorType;
	*outStream << msgId;
    }
    static void sendError( ILeamOutStream* outStream, TMessageId msgId, const std::string& _st, const TDataVector& data )
    {
	beginError( (TType)ELeamTypeError, outStream, msgId );
	TLength total = _st.length() + data.size() + sizeof( TLength );
	*outStream << total;
	*outStream << (TLength)_st.length();
	*outStream << _st;
	*outStream << data;
	outStream->flush();
    }
    template< typename TFuncType >
    static bool wrapUserMethod( ILeamOutStream* outStream, TMessageId msgId, TFuncType func )
    {
	try {
	    func();
	    return true;
	}
	catch( const LeamUserError& error )
	{
	    sendError( outStream, msgId, error.getStackTrace(), error.getErrorData() );
	}
	catch( const licada::LicadaError& error )
	{
	    sendError( outStream, msgId, error.getStackTrace(), TDataVector() );
	}
	catch( ... )
	{
	    sendError( outStream, msgId, std::string(), TDataVector() );
	}
	return false;
    }
    static void handleMessage( ILeamHandler* handler
			       , TMessageId msgId 
			       , LeamDataInStream& dataInStream
			       , ILeamOutStream* outStream )
    {
	handler->handleData( msgId, dataInStream );
	*outStream << (TType)ELeamTypeFinished;
	*outStream << msgId;
	outStream->flush();
    }
    void LeamStream::handleError(TMessageId msgId)
    {
	wrapUserMethod( outStream, msgId
			, bind( &ILeamHandler::handleError, handler, msgId, stackTrace, ref(dataIn) ) );
	dataIn.ignore( dataIn.getRemaining());
	erase_if( unAck, bind( std::equal_to<TMessageId>(), _1, msgId ));
	erase_if( unFin, bind( std::equal_to<TMessageId>(), _1, msgId ));
    }
    //Wait for the next bit of communication
    void LeamStream::readNext()
    {
	if ( inStream->hasData() == false )
	    return;
	TType type;
	TMessageId msgId;
	TLength length;
	TLength secondLength;
	bool success;
	TMessageIdVector::iterator iter;
	*inStream >> type;
	switch( type )
	{
	case ELeamTypeMessage:
	    *inStream >> msgId;
	    *inStream >> length;
	    dataIn.setup( length, inStream );
	    *outStream << (TType)ELeamTypeAck;
	    *outStream << msgId;
	    outStream->flush();
	    success = wrapUserMethod( outStream, msgId
				      , bind( handleMessage
					      , handler, msgId
					      , ref(dataIn), outStream ) );
	    dataIn.ignore( dataIn.getRemaining());
	    break;
	case ELeamTypeAck:
	    *inStream >> msgId;
	    erase_if( unAck, bind( std::equal_to<TMessageId>(), _1, msgId ));
	    wrapUserMethod( outStream, msgId
			    , bind( &ILeamHandler::handleAck, handler, msgId ));
	    break;
	case ELeamTypeFinished:
	    *inStream >> msgId;
	    erase_if( unFin, bind( std::equal_to<TMessageId>(), _1, msgId ));
	    wrapUserMethod( outStream, msgId
			    , bind(&ILeamHandler::handleFin, handler, msgId ) );
	    break;
	case ELeamTypeError:
	    *inStream >> msgId;
	    *inStream >> length;
	    *inStream >> secondLength;
	    stackTrace.resize(secondLength);
	    *inStream >> stackTrace;
	    dataIn.setup( length - secondLength - sizeof( TLength), inStream );
	    handleError(msgId);
	    break;
	case ELeamTypeClosed:
	    wrapUserMethod( outStream, -1
			    , bind( &ILeamHandler::handleClose, handler) );
	    break;
	default:
	    throw UnrecognizedMessageType( type );
	}
    }
    void LeamStream::readAll()
    {
	while( inStream->hasData() )
	    readNext();
    }
    //Queries whether a message was acknowledged
    //Keep in mind that the message id increments, and all this does
    //is see if the message is the unacknowledged list.
    bool LeamStream::isAck( TMessageId msg ) { 
	return std::find( unAck.begin(), unAck.end(), msg ) == unAck.end(); }
    //Queries whether a message was processed
    bool LeamStream::isFin( TMessageId msg ) { 
	return std::find( unFin.begin(), unFin.end(), msg ) == unFin.end(); }
    
    //close in and out streams, tell handler we are closed
    void LeamStream::close()
    {
	*outStream << (TType)ELeamTypeClosed;
	outStream->flush();
	outStream->close();
	outStream = NULL;
	inStream->close();
	inStream = NULL;
    }
}
